/*
* $Id$
*
* SARL is an general-purpose agent programming language.
* More details on http://www.sarl.io
*
* Copyright (C) 2014-2017 the original authors or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.sarl.maven.docs;
import java.io.File;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Properties;
import java.util.Set;
import java.util.TreeMap;
import com.google.common.base.Throwables;
import com.google.common.collect.Lists;
import com.google.common.io.Files;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.name.Names;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.DependencyResolutionRequiredException;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.descriptor.PluginDescriptor;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.apache.maven.shared.utils.io.DirectoryScanner;
import org.apache.maven.toolchain.Toolchain;
import org.apache.maven.toolchain.ToolchainManager;
import org.apache.maven.toolchain.ToolchainPrivate;
import org.apache.maven.toolchain.java.JavaToolChain;
import org.arakhne.afc.vmutil.FileSystem;
import org.codehaus.plexus.util.xml.Xpp3Dom;
import org.eclipse.xtext.Constants;
import org.eclipse.xtext.util.JavaVersion;
import org.eclipse.xtext.util.Strings;
import org.eclipse.xtext.xbase.lib.util.ReflectExtensions;
import io.sarl.maven.docs.markdown.MarkdownParser;
import io.sarl.maven.docs.parser.AbstractMarkerLanguageParser;
import io.sarl.maven.docs.parser.SarlDocumentationParser;
import io.sarl.maven.docs.parser.SarlDocumentationParser.ParsingException;
import io.sarl.maven.docs.testing.DocumentationSetup;
import io.sarl.maven.docs.testing.ScriptExecutor;
/** Abstract Maven MOJO for the documentation of the SARL project.
*
* @author $Author: sgalland$
* @version $FullVersion$
* @mavengroupid $GroupId$
* @mavenartifactid $ArtifactId$
* @since 0.6
*/
public abstract class AbstractDocumentationMojo extends AbstractMojo {
/** Name of the default source directory.
*/
public static final String DEFAULT_SOURCE_DIRECTORY = "src/main/documentation"; //$NON-NLS-1$
/**
* Location of the temp directory.
*/
@Parameter(defaultValue = "${basedir}/target/documentation-temp", required = true)
protected File tempDirectory;
/**
* Location of the generated folder of the test sources.
*/
@Parameter(defaultValue = "${basedir}/target/generated-documentation-sources", required = true)
protected String testSourceDirectory;
/**
* Location of the source directories.
*/
@Parameter
protected List<String> sourceDirectories;
/** Default file encoding.
*/
@Parameter(defaultValue = "${project.build.sourceEncoding}", required = true)
protected String encoding;
/**
* Indicates if the source directories provided by Maven
* must be ignored if {@link #sourceDirectories} are manually provided.
*/
@Parameter(defaultValue = "true", required = true)
protected boolean overrideSourceDirectories;
/**
* The project itself. This parameter is set by maven.
*/
@Parameter(required = true, defaultValue = "${project}", readonly = true)
protected MavenProject project;
/**
* The base directory.
*/
@Parameter(required = true, defaultValue = "${basedir}", readonly = true)
protected File baseDirectory;
/**
* File extensions, including the dot character. Default is the file extension of the Markdown format.
*/
@Parameter(required = false)
protected List<String> fileExtensions;
/** File extension for the target language.
*/
protected String targetLanguageFileExtension;
/**
* The current Maven session.
*/
@Parameter(defaultValue = "${session}", required = true, readonly = true)
protected MavenSession session;
/**
* Indicates if the Github extension should be enabled.
*/
@Parameter(defaultValue = "false", required = false)
protected boolean githubExtension;
/**
* Indicates if the line continuation syntax is enabled or not.
*/
@Parameter(defaultValue = "true", required = false)
protected boolean isLineContinuationEnable;
/**
* Java version number to support.
*/
@Parameter(required = false)
protected String source;
/** Injector.
*/
protected Injector injector;
/** Inferred source directories.
*/
protected List<String> inferredSourceDirectories;
@Component
private ToolchainManager toolchainManager;
private ReflectExtensions reflect;
private static boolean isFileExtension(File filename, String[] extensions) {
final String extension = FileSystem.extension(filename);
for (final String ext : extensions) {
if (Strings.equal(ext, extension)) {
return true;
}
}
return false;
}
/** Replies the message to display for skipping the execution of this mojo.
* If this function does not reply a message, the execution is not skipped.
* If this function replies a message, the execution is skipped.
*
* @return A message for skipping the execution, or {@code null} for executing the mojo.
*/
protected abstract String getSkippingMessage();
@Override
public final void execute() throws MojoExecutionException {
final String skipMessage = getSkippingMessage();
if (!Strings.isEmpty(skipMessage)) {
getLog().info(skipMessage);
return;
}
if (this.fileExtensions == null || this.fileExtensions.isEmpty()) {
this.fileExtensions = Arrays.asList(MarkdownParser.MARKDOWN_FILE_EXTENSIONS);
}
if (this.sourceDirectories == null) {
this.sourceDirectories = Collections.emptyList();
}
if (!this.sourceDirectories.isEmpty() && this.overrideSourceDirectories) {
this.inferredSourceDirectories = Lists.newArrayList(this.sourceDirectories);
} else {
this.inferredSourceDirectories = Lists.newArrayList(this.project.getCompileSourceRoots());
this.inferredSourceDirectories.addAll(this.sourceDirectories);
this.inferredSourceDirectories.add(DEFAULT_SOURCE_DIRECTORY);
}
getLog().info(Messages.AbstractDocumentationMojo_0);
this.injector = DocumentationSetup.doSetup();
assert this.injector != null;
if (this.reflect == null) {
this.reflect = this.injector.getInstance(ReflectExtensions.class);
}
this.targetLanguageFileExtension = this.injector.getInstance(Key.get(String.class, Names.named(Constants.FILE_EXTENSIONS)));
final String errorMessage = internalExecute();
if (!Strings.isEmpty(errorMessage)) {
throw new MojoExecutionException(errorMessage);
}
}
/** Internal run.
*
* @return the error message
*/
protected String internalExecute() {
getLog().info(Messages.AbstractDocumentationMojo_1);
final Map<File, File> files = getFiles();
getLog().info(MessageFormat.format(Messages.AbstractDocumentationMojo_2, files.size()));
return internalExecute(files);
}
/** Internal run.
*
* @param files the map from source file to the source folder.
* @return the error message
*/
@SuppressWarnings("static-method")
protected String internalExecute(Map<File, File> files) {
throw new UnsupportedOperationException();
}
/** Execute the mojo on the given set of files.
*
* @param files the files
* @param outputFolder the output directory.
* @return the error message
*/
protected String internalExecute(Map<File, File> files, File outputFolder) {
String firstErrorMessage = null;
for (final Entry<File, File> entry : files.entrySet()) {
final File inputFile = entry.getKey();
try {
final AbstractMarkerLanguageParser parser = createLanguageParser(inputFile);
final File sourceFolder = entry.getValue();
final File relativePath = FileSystem.makeRelative(inputFile, sourceFolder);
internalExecute(sourceFolder, inputFile, relativePath, outputFolder, parser);
} catch (Throwable exception) {
final String errorMessage = formatErrorMessage(inputFile, exception);
getLog().error(errorMessage);
if (Strings.isEmpty(firstErrorMessage)) {
firstErrorMessage = errorMessage;
}
getLog().debug(exception);
}
}
return firstErrorMessage;
}
/** Execute the mojo on the given set of files.
*
* @param sourceFolder the source folder.
* @param inputFile the input file.
* @param relativeInputFile the name of the input file relatively to the source folder.
* @param outputFolder the output folder.
* @param parser the parser to be used for reading the input file.
* @throws IOException if there is some issue with IO.
*/
@SuppressWarnings("static-method")
protected void internalExecute(File sourceFolder, File inputFile, File relativeInputFile, File outputFolder,
AbstractMarkerLanguageParser parser)
throws IOException {
throw new UnsupportedOperationException();
}
/** Format the error message.
*
* @param inputFile the input file.
* @param exception the error.
* @return the error message.
*/
protected String formatErrorMessage(File inputFile, Throwable exception) {
File filename;
int lineno = 0;
final boolean addExceptionName;
if (exception instanceof ParsingException) {
addExceptionName = false;
final ParsingException pexception = (ParsingException) exception;
final File file = pexception.getFile();
if (file != null) {
filename = file;
} else {
filename = inputFile;
}
lineno = pexception.getLineno();
} else {
addExceptionName = true;
filename = inputFile;
}
for (final String sourceDir : this.session.getCurrentProject().getCompileSourceRoots()) {
final File root = new File(sourceDir);
if (isParentFile(filename, root)) {
try {
filename = FileSystem.makeRelative(filename, root);
} catch (IOException exception1) {
//
}
break;
}
}
final StringBuilder msg = new StringBuilder();
msg.append(filename.toString());
if (lineno > 0) {
msg.append(":").append(lineno); //$NON-NLS-1$
}
msg.append(": "); //$NON-NLS-1$
final Throwable rootEx = Throwables.getRootCause(exception);
if (addExceptionName) {
msg.append(rootEx.getClass().getName());
msg.append(" - "); //$NON-NLS-1$
}
msg.append(rootEx.getLocalizedMessage());
return msg.toString();
}
private static boolean isParentFile(File file, File root) {
if (file.isAbsolute() && root.isAbsolute()) {
try {
final String[] components1 = FileSystem.split(file.getCanonicalFile());
final String[] components2 = FileSystem.split(root.getCanonicalFile());
for (int i = 0; i < components2.length; ++i) {
if (i >= components1.length || !Strings.equal(components2[i], components1[i])) {
return false;
}
}
return true;
} catch (IOException exception) {
//
}
}
return false;
}
/** Create a parser for the given file.
*
* @param inputFile the file to be parsed.
* @return the parser.
* @throws MojoExecutionException if the parser cannot be created.
* @throws IOException if a classpath entry cannot be found.
*/
protected AbstractMarkerLanguageParser createLanguageParser(File inputFile) throws MojoExecutionException, IOException {
final AbstractMarkerLanguageParser parser;
if (isFileExtension(inputFile, MarkdownParser.MARKDOWN_FILE_EXTENSIONS)) {
parser = this.injector.getInstance(MarkdownParser.class);
} else {
throw new MojoExecutionException(MessageFormat.format(Messages.AbstractDocumentationMojo_3, inputFile));
}
parser.setGithubExtensionEnable(this.githubExtension);
final SarlDocumentationParser internalParser = parser.getDocumentParser();
if (this.isLineContinuationEnable) {
internalParser.setLineContinuation(SarlDocumentationParser.DEFAULT_LINE_CONTINUATION);
} else {
internalParser.addPropertyProvider(createProjectProperties());
}
final ScriptExecutor scriptExecutor = internalParser.getScriptExecutor();
final StringBuilder cp = new StringBuilder();
for (final File cpElement : getClassPath()) {
if (cp.length() > 0) {
cp.append(":"); //$NON-NLS-1$
}
cp.append(cpElement.getAbsolutePath());
}
scriptExecutor.setClassPath(cp.toString());
final String bootPath = getBootClassPath();
if (!Strings.isEmpty(bootPath)) {
scriptExecutor.setBootClassPath(bootPath);
}
JavaVersion version = null;
if (!Strings.isEmpty(this.source)) {
version = JavaVersion.fromQualifier(this.source);
}
if (version == null) {
version = JavaVersion.JAVA8;
}
scriptExecutor.setJavaSourceVersion(version.getQualifier());
scriptExecutor.setTempFolder(this.tempDirectory.getAbsoluteFile());
internalParser.addPropertyProvider(createProjectProperties());
internalParser.addPropertyProvider(this.session.getCurrentProject().getProperties());
internalParser.addPropertyProvider(this.session.getUserProperties());
internalParser.addPropertyProvider(this.session.getSystemProperties());
internalParser.addPropertyProvider(createGeneratorProperties());
return parser;
}
private Properties createProjectProperties() {
final Properties props = new Properties();
final MavenProject prj = this.session.getCurrentProject();
props.put("project.groupId", prj.getGroupId()); //$NON-NLS-1$
props.put("project.artifactId", prj.getArtifactId()); //$NON-NLS-1$
props.put("project.basedir", prj.getBasedir()); //$NON-NLS-1$
props.put("project.description", prj.getDescription()); //$NON-NLS-1$
props.put("project.id", prj.getId()); //$NON-NLS-1$
props.put("project.inceptionYear", prj.getInceptionYear()); //$NON-NLS-1$
props.put("project.name", prj.getName()); //$NON-NLS-1$
props.put("project.version", prj.getVersion()); //$NON-NLS-1$
props.put("project.url", prj.getUrl()); //$NON-NLS-1$
props.put("project.encoding", this.encoding); //$NON-NLS-1$
return props;
}
private Properties createGeneratorProperties() {
final Properties props = new Properties();
final PluginDescriptor descriptor = (PluginDescriptor) getPluginContext().get("pluginDescriptor"); //$NON-NLS-1$
props.put("generator.name", descriptor.getArtifactId()); //$NON-NLS-1$
props.put("generator.version", descriptor.getVersion()); //$NON-NLS-1$
return props;
}
/** Replies the source files.
*
* @return the map from the source files to the corresponding source folders.
*/
protected Map<File, File> getFiles() {
final Map<File, File> files = new TreeMap<>();
for (final String rootName : this.inferredSourceDirectories) {
File root = FileSystem.convertStringToFile(rootName);
if (!root.isAbsolute()) {
root = FileSystem.makeAbsolute(root, this.baseDirectory);
}
getLog().debug(MessageFormat.format(Messages.AbstractDocumentationMojo_4, root.getName()));
for (final File file : Files.fileTreeTraverser().breadthFirstTraversal(root)) {
if (file.exists() && file.isFile() && !file.isHidden() && file.canRead() && hasExtension(file)) {
files.put(file, root);
}
}
}
return files;
}
private boolean hasExtension(File file) {
final String extension = FileSystem.extension(file);
return this.fileExtensions.contains(extension);
}
/** Convert a file to a package name.
*
* @param rootPackage an additional root package.
* @param packageName the file.
* @return the package name.
*/
protected static String toPackageName(String rootPackage, File packageName) {
final StringBuilder name = new StringBuilder();
File tmp = packageName;
while (tmp != null) {
final String elementName = tmp.getName();
if (!Strings.equal(FileSystem.CURRENT_DIRECTORY, elementName)
&& !Strings.equal(FileSystem.PARENT_DIRECTORY, elementName)) {
if (name.length() > 0) {
name.insert(0, "."); //$NON-NLS-1$
}
name.insert(0, elementName);
}
tmp = tmp.getParentFile();
}
if (!Strings.isEmpty(rootPackage)) {
if (name.length() > 0) {
name.insert(0, "."); //$NON-NLS-1$
}
name.insert(0, rootPackage);
}
return name.toString();
}
/** Convert a a package name for therelative file.
*
* @param packageName the name.
* @return the file.
*/
protected static File toPackageFolder(String packageName) {
File file = null;
for (final String element : packageName.split("[.]")) { //$NON-NLS-1$
if (file == null) {
file = new File(element);
} else {
file = new File(file, element);
}
}
return file;
}
/** Replies the current classpath.
*
* @return the current classpath.
* @throws IOException on failure.
*/
protected List<File> getClassPath() throws IOException {
final Set<String> classPath = new LinkedHashSet<>();
classPath.add(this.session.getCurrentProject().getBuild().getSourceDirectory());
try {
classPath.addAll(this.session.getCurrentProject().getCompileClasspathElements());
} catch (DependencyResolutionRequiredException e) {
throw new IOException(e.getLocalizedMessage(), e);
}
for (final Artifact dep : this.session.getCurrentProject().getArtifacts()) {
classPath.add(dep.getFile().getAbsolutePath());
}
classPath.remove(this.session.getCurrentProject().getBuild().getOutputDirectory());
final List<File> files = new ArrayList<>();
for (final String filename : classPath) {
final File file = new File(filename);
if (file.exists()) {
files.add(file);
}
}
return files;
}
/** Replies the boot classpath.
*
* @return the boot classpath.
* @throws IOException in case of error.
*/
protected String getBootClassPath() throws IOException {
final Toolchain toolchain = this.toolchainManager.getToolchainFromBuildContext("jdk", this.session); //$NON-NLS-1$
if (toolchain instanceof JavaToolChain && toolchain instanceof ToolchainPrivate) {
final JavaToolChain javaToolChain = (JavaToolChain) toolchain;
final ToolchainPrivate privateJavaToolChain = (ToolchainPrivate) toolchain;
String[] includes = {"jre/lib/*", "jre/lib/ext/*", "jre/lib/endorsed/*"}; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
String[] excludes = new String[0];
final Xpp3Dom config = (Xpp3Dom) privateJavaToolChain.getModel().getConfiguration();
if (config != null) {
final Xpp3Dom bootClassPath = config.getChild("bootClassPath"); //$NON-NLS-1$
if (bootClassPath != null) {
final Xpp3Dom includeParent = bootClassPath.getChild("includes"); //$NON-NLS-1$
if (includeParent != null) {
includes = getValues(includeParent.getChildren("include")); //$NON-NLS-1$
}
final Xpp3Dom excludeParent = bootClassPath.getChild("excludes"); //$NON-NLS-1$
if (excludeParent != null) {
excludes = getValues(excludeParent.getChildren("exclude")); //$NON-NLS-1$
}
}
}
try {
return scanBootclasspath(Objects.toString(this.reflect.invoke(javaToolChain, "getJavaHome")), includes, excludes); //$NON-NLS-1$
} catch (Exception e) {
throw new IOException(e.getLocalizedMessage(), e);
}
}
return ""; //$NON-NLS-1$
}
private static String scanBootclasspath(String javaHome, String[] includes, String[] excludes) {
final DirectoryScanner scanner = new DirectoryScanner();
scanner.setBasedir(new File(javaHome));
scanner.setIncludes(includes);
scanner.setExcludes(excludes);
scanner.scan();
final StringBuilder bootClassPath = new StringBuilder();
final String[] includedFiles = scanner.getIncludedFiles();
for (int i = 0; i < includedFiles.length; i++) {
if (i > 0) {
bootClassPath.append(File.pathSeparator);
}
bootClassPath.append(new File(javaHome, includedFiles[i]).getAbsolutePath());
}
return bootClassPath.toString();
}
private static String[] getValues(Xpp3Dom[] children) {
final String[] values = new String[children.length];
for (int i = 0; i < values.length; i++) {
values[i] = children[i].getValue();
}
return values;
}
}